今天，我们继续这个建模过程，首先考虑“在子项目上报工时”这个需求的更多细节，增加更多的灵活性。

然后，我们来考虑另一个需求——“为内部项目报工时”。我们会采用使用泛化和不使用泛化两种方式为这个需求建模，并且比较两者的异同，以便你更进一步理解泛化的思路。事实上，用不用泛化有时不是一个非黑即白的决定，而是一种权衡。


## 增强灵活性
我们先回顾一下上节课建立的模型。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211215037.png"/>


到现在为止，我们的模型已经基本可以满足按项目、子项目和普通工时项来报工时的需求了。

我们简单复述一下目前的需求。
* “零零后公司”只能按项目登记工时。
* “九零后公司”只能按子项目登记工时。
* “八零后公司”既可以按项目也可按子项目，还可以按普通工时项登记工时。

现在我开始向你提几个更深入的问题。

第一个问题是：怎么控制“零零后公司”只能按项目，而不能按子项目报工时呢？反之，怎么控制“九零后公司”只能按子项目报工时呢？

为了解决这个问题，你决定为每个租户增加一个配置参数。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211215220.png"/>

所有租户级的参数，都在租户参数这个实体里。我们商量了一下，把新的参数命名为项目工时粒度，有三个选项，分别是：“项目”“子项目”和“两可”。对于“零零后公司”只要把这个参数设置成“项目”，就说明只能按项目来报工时。对于其他两个公司的需求，道理是一样的。


这样也解决了另一个问题，就是，万一“零零后公司”将来也想按子项目来报工时的话，我们只需要改一下这个参数的设置就可以了。


我的第二个问题是：对于多数公司，按普通工时项报工时应该是一个很普遍的需求。虽然“零零后公司”现在不需要这样报工时，但将来如果要，怎么办呢？




这时候你又反问了我一个问题：“八零后公司”既可以按项目也可按子项目来报工时，也就是参数里的“两可”。但是“两可”的具体含义是什么呢？因为其实可以有两种不同的理解。


第一种理解是，任何项目都“两可”。比如有一个项目 A，那么每一个员工，既可以把工时报到项目 A 整体上，而不区分子项目，也可以把工时报到项目 A 的某个子项目上，完全取决于员工自己的选择。


第二种理解是，对于某一个具体项目而言，只能在“按项目”和“按子项目”中选择一种。例如，项目 A，只能按项目整体报工时；项目 B，只能按子项目报工时。而对于整个公司而言，由于同时存在 A 和 B 两种情况，所以在整体上是“两可”。


我思考了一下，说：“确实有这两种细微的差别。但是如果在租户的参数上，再区分这两种情况的话，可能反而会把用户搞糊涂。”

事实上，之所以有“两可”这个选项，无非是想给项目自身一个灵活性，如果把这个选项放在项目级别，由项目经理来决定，似乎更合适。但是全局的参数仍然可以保留，作为默认值，具体项目可以修改默认值。基于这个思路，你尝试这样修改了模型。


<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211215737.png"/>

你对模型做了三点改动。
* 第一，把租户参数里原来的“项目工时粒度”，改成了“默认项目工时粒度”。也就是说，这个选项可以被具体的项目覆盖。
* 第二，你用一个注解，明确了“两可”的含义，也就是你所说的第一种理解：“两可”指的是，对于一个项目，既可以把工时报在项目整体上，也可以报在它的子项目上，取决于员工自己的选择。
* 第三，在项目实体上增加了一个工时粒度属性，它的可选项和租户参数里是一样的。这样就可以在项目中进行细粒度的配置了。


有了这几个改动，就可以在租户参数里设置最常用的选项作为默认值，然后让项目经理在项目级别决定是否要覆盖默认值。这样就能满足“八零后公司”的需求了。

但是这样的话，对于“零零后公司”，岂不是也可以由项目经理自行配置了？这显然不符合当前的需求。


于是你又增加了一个租户级别的参数。
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211215952.png"/>

这个参数就是“是否可覆盖默认项目工时粒度”。对于“零零后公司”和“九零后公司”，把这个参数设为“否”，对于“八零后公司”设为“是”。这样一来，就可以同时满足这三个公司的不同需求了。

而且，不只是这三家公司，对于更多的公司的类似需求来说，上面的设计也足以满足了。当然，如果以后再遇到更“古怪”的需求，我们可以遵循类似的思路去增加系统的灵活性。


我们再思考一下，前面的做法算不算“过度设计”呢？

我的看法是这样的：如果是给某一家公司量身定做一个系统的话，本来不需要考虑这么多。但作为一个 SaaS 系统，要面对各种不同的用户，所以就要把系统设计得灵活一些了。假如局限于某个特定的公司，这样做可能有“过度设计”的嫌疑。但对于 SaaS 应用，这样的设计应该是恰当的。


## 非客户项目的泛化
现在，我们再来考虑“九零后公司”的另一个需求，并尝试用泛化来建模。


这个需求是这样的：九零后公司有一些公司内部的项目，比如说几个小伙伴一起搞一个内部开源的项目。和客户项目不同，这些内部项目没有合同，也不需要专门分配人力，而是谁有空，就可以随时参与。


于是你问我：“是不是可以说，客户项目和内部项目统称为项目，他们的共性是都可以报工时？”

我说：“可以这么理解。”


你紧跟着问：“这么说，内部项目岂不就是普通工时项？是不是可以直接用普通工时项实现这个需求呢？”

我想了一下，说：“内部项目和客户项目还有更多的共性，是普通工时项所不具备的，比如说，作为项目，内部项目也有项目经理，也有子项目，也有明确的起止时间，也有项目状态等等。”

你觉得言之有理，于是利用泛化的知识，画出了后面的模型图。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211220340.png"/>


从这个图里，我们看到，作为父类的项目实体，反映了客户项目和内部项目的共性，例如都有项目经理，都可以建立子项目等等；而客户项目体现了与内部项目的区别或者说个性，例如一个客户项目必须属于某个合同，并且需要分配项目成员，而内部项目则不具有这两个特征。所以，目前这个模型确实反映了需求中的领域知识。



## 一定要泛化吗？
我仔细看了这个模型，然后又问了一个问题：“我承认这个模型正确地反映了需求，但是泛化似乎让这个模型变得更复杂了，有没有可能画一个不用泛化的模型，看看效果怎么样呢？”

你说：“也可以，我们比较一下吧。”

于是，你又画出了后面这个等价的模型。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211220457.png"/>

可以看到，这个模型也正确地反映了同样的需求。如果把这个模型和没有反映“客户项目和内部项目”需求的模型，也就是这节课的第一张图相对比，就会发现，这个模型只有三个主要变化。

第一，在项目与合同的关联上，合同一端的多重性由“1..1”变成了“0..1”。这是因为，原来，一个项目必须属于某个合同，而现在，内部项目没有合同，也就是说，有的项目可以对应“0”个合同。

第二，在项目实体中增加了一个“是否客户项目”的标志，这样就代替了原来泛化符号起到的区分两种项目的作用。

第三，增加了两个约束，用于说明客户项目和内部项目的区别。而在采用泛化的模型中，并不需要这两个约束，因为模型本身的结构已经说明了相同的知识。

这时候我说：“站在产品经理的角度，不用泛化的模型，比起之前没有这个新需求的模型（“增强灵活性”一节的最后一张图）来说，并没有增加或者减少任何关联，而且由于没有用泛化，更简单，更容易理解，你觉得呢？”

你说：“从架构师的角度，这个模型也更容易实现。既然这样，咱们还是不用泛化了吧。” 于是我们达成了一致。

## 继续增强灵活性

你又看了一下和“是否客户项目”有关的业务规则，然后问了我另一个问题：“‘是否客户项目’和‘是否要分配项目成员’，是同一个维度的问题吗？比如说，会不会内部项目也要分配成员呢？”


我想了想说：“你说得对。尽管九零后公司自己认为这是一回事，但如果把这个应用卖给其他用户，可能就会出现内部项目也要分配成员的情况。也就是说，现在的模型还缺乏通用性。所以，把这两者当做两个维度，分开设置比较好。”

于是你又修改了模型。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211220736.png"/>


在项目里增加了一个“是否要分配项目成员”的属性，和“是否客户项目”分开。然后，在原来关于“是否客户项目”的注释里，删除了有关分配项目成员的约束，因为“是否要分配项目成员”这个属性本身已经不言自明了。

这时候我问了你今天最后一个问题：“回过头来想一下，如果把‘是否客户项目’和‘是否要分配项目成员’都用泛化的方式表示，模型会变成什么样子呢？”

于是，你画出了后面的模型。
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241211221441.png"/>

比起前面的图，这个模型更加复杂，理解起来更困难一些。所以，我们决定还是选择前面简单些的模型。

另外，这个模型实际上是按照两个不同的维度进行泛化的，称为“多重泛化”。我们在第三个迭代再来讨论这种复杂的情况。


继承通常只能解决单维度的泛化问题，多重泛化有多个维度。
## 总结

今天，为了实现“为内部项目报工时”引发的需求，我们比较了使用泛化和不使用泛化两种方式。最后选择了从业务和技术视角都更加简单的，不使用泛化的方式。这说明，使用或者不使用泛化，是一个需要权衡的决策。在权衡过程中，需要结合业务和技术视角。


另外，通过为两个需求增加灵活性，我们也体会到，相比给特定企业定制的系统，SaaS 系统往往需要考虑更多的灵活性。


